組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

2.1 オブジェクト指向プログラミングの概要

C++といえばオブジェクト指向,あるいはオブジェクト指向をで開発するのであればC++を使うべき,という意見をよく耳にします.そうした意見が正しいかどうかはさておき,「オブジェクト指向」がいったい何なのかを理解していないことには,これ以上先に進むことができません.ここでは,オブジェクト指向,特にオブジェクト指向プログラミングの大まかな概念について解説します.

オブジェクト指向については,詳しく書き始めるとそれだけで1冊の本になってしまいますし,人によっても微妙に考え方が異なります.本書はオブジェクト指向の解説書ではありませんので,C++をうまく活用するうえで必要な内容にできるだけ絞って,オブジェクト指向を解説したいと思います.

2.1.1 オブジェクト指向分析/オブジェクト指向設計/ オブジェクト指向プログラミング

「オブジェクト指向」は,もともとは「オブジェクト指向プログラミング」から始まり,やがて「オブジェクト指向設計」,「オブジェクト指向分析」というように,より上流の工程でも応用されるようになっていきました.

オブジェクト指向“分析”は,要求分析のための手法ですので,CとかC++といった開発言語とは直接関係ありません.したがって,どんな言語で実装するかにかかわらず,オブジェクト指向分析の手法を使うことが可能です.ただし,どんなものでもオブジェクト指向で要求分析を行えばよいかというとそうでもなく,場合によっては構造化分析のほうが適していることもあります.

オブジェクト指向“設計”は,ソフトウェアの内部的な実現方法を定義するための手法です.現実的には,ある程度開発言語を意識せざるをえない部分があることも確かですが,基本的には,開発言語と分離して行うことが可能です.したがって,オブジェクト指向で設計を行い,Cやアセンブリ言語で実装することも可能です.具体的には,組込みとはやや異なりますが,世の中に多数出回っているGUIツールキットがそうです.GUIツールキットは,明らかにオブジェクト指向で設計されていても,実装言語はCであることが多々あります.

もうひとつ,オブジェクト指向が用いられる分野が,当然のことのようですが,「オブジェクト指向プログラミング」です.ある程度の制限はあるものの,開発言語にかかわらず,オブジェクト指向プログラミングを行うことが可能です.オブジェクト指向プログラミングに適しているとされる,いわゆる「オブジェクト指向プログラミング言語」には,オブジェクト指向プログラミングの概念を直接表現するための言語機能が備わっています.

このように,オブジェクト指向とC++は直接は関係ありません.しかし,オブジェクト指向とC++の相性が良いことは確かです.完全にオブジェクト指向で開発するわけではないとしても,部分的にもオブジェクト指向の方法を採り入れることによって,C++の利点をうまく引き出すことができることでしょう.

2.1.2 構造化プログラミングと構造化プログラミング言語

オブジェクト指向プログラミングの解説に入る前に,ウォーミングアップとして,構造化プログラミングをおさらいしてみることにしましょう.構造化プログラミングでは,プログラム全体を「サブルーチン」に分割します.そして,大まかな流れを記述したメインルーチンからサブルーチンを呼び出します.サブルーチンは,「順次」,「反復」,「分岐」という3つの論理構造の組み合わせによって記述します.

構造化プログラミングをCに当てはめると,「サブルーチン」は「関数」に,「順次」は「文」に,「反復」はfor文やdo文のような「繰返し文」に,「分岐」はif文などの「選択文」によって表現することができます.このように,Cは,構造化プログラミングの構成要素をソースコードにマッピングするための言語機能が備わっているため,「構造化プログラミング言語」に分類されています.

では,アセンブリ言語や古いBASICのような「非構造化プログラミング言語」では構造化プログラミングができないかというと,決してそんなことはありません.ただし,構造化プログラミングの構成要素をソースコードにどのようにマッピングするかについて,独自のルールを取り決める必要が出てきます.当然それだけ手間がかかりますし,独自に設けたルールに慣れるまでは,ソースコードを読むのに少々手間取ることでしょう.

オブジェクト指向プログラミングとオブジェクト指向プログラミング言語の関係も,ある意味でこれと同じことがいえます.C++には,オブジェクト指向プログラミングの構成要素をソースコードにマッピングするための言語機能が備わっています.そうした機能を使えば,簡単にオブジェクト指向プログラミングを行うことができますし,ソースコードの可読性も上がります.しかし,多少の不便を覚悟するのであれば,Cでもオブジェクト指向プログラミングは可能というわけです.

2.1.3 オブジェクト指向プログラミングの概要

それでは,いよいよオブジェクト指向プログラミングの解説に入ります.構造化プログラミングの場合と同様,オブジェクト指向プログラミングにもいくつかの構成要素があります.しかし,人によって,オブジェクト指向プログラミングの構成要素として取り上げる内容が若干異なるために,やや話が複雑になっています.ここでは,あまり細かいことは気にせず,実用本位での解説を心がけることにします.

抽象化

オブジェクト指向プログラミングに限らず,プログラミングを行ううえで,「抽象化」というのは非常に重要な考え方です.構造化プログラミングにおけるサブルーチンも,処理を抽象化したものにほかなりません.

抽象化は,処理だけでなく,データに関しても行うことができます.

最も単純な例を考えてみましょう.Cの標準ライブラリには,time_t型という暦時間を格納するための型が存在します.この型は,算術型であることだけが規定されています*1.したがって,その値や実際の型が何であるかに依存するようなコードは記述すべきではなく,年月日時分秒に分解するにはgmtimeやlocaltime関数を,暦時刻の差を求めるにはdifftime関数を使わなければなりません.これも一種のデータ抽象化です.

別の例を挙げてみましょう.μITRONは組込み開発でよく用いられるリアルタイムカーネルですが,そのサービスコールの多くは,ID番号によってカーネルオブジェクトを指定します.ID番号は単なる整数値ですが,カーネル内部のデータ構造を知ることなく,タスクを起動/停止したり,セマフォを獲得/解放することができるのです.このように,データ構造の実体である「ボディ」(ここではカーネルオブジェクト)と,それを指定するための「ハンドル」(ここではID番号)を用いてデータを抽象化する手法は,オブジェクト指向プログラミングに限らず,広く使われています.

もう1つ例を挙げてみましょう.今度は,標準CライブラリのFILE型です.FILE型もハンドルボディ形式の抽象化の一種です.しかし,FILE型を用いて操作するのはデータだけではありません.FILE型のハンドルに結び付けられているのは,(ディスク上の)ファイルであったり,キーボードやディスプレイであったり,シリアルポートやプリンタであったりします.すなわち,FILE型は,データだけでなく,デバイスをも抽象化しているわけです.

抽象化を適切に行うことで,関数やデータがどのように実装されているかを知らなくても,それらを利用することができるようになります*2.オブジェクト指向プログラミングは,この抽象化が出発点となります.

*1 POSIXでは,「1970年1月1日 00:00:00 UTC」からの秒数であることが規定されていますが,標準Cでは何も規定されていません.

*2 現実には,デバッグのときなど,実装の詳細を知らなければどうしようもない状況もありえます.しかし,つねに実装の詳細を意識しなければならないデリケートな設計より,(建前だけでも)実装の詳細を知らずに済む設計のほうが優れていることは間違いありません.

オブジェクトとクラス

オブジェクト指向というのは,文字どおり「オブジェクト」に重点を置く手法です.

では,オブジェクトとは何かというと,オブジェクト指向プログラミングにおいては,プログラムの中で扱う「もの」あるいは「対象」のことです*3.C/C++の言語仕様にも「オブジェクト」という概念がありますが,これは大ざっぱにいえば変数のことです.C/C++のオブジェクトも,オブジェクト指向プログラミングにおけるオブジェクトの一種なのでしょうが,普通は分けて考えます.

それに対して,「クラス」というのは,オブジェクトの特徴や性質のことです.

たとえば,次のようにいえば,ポチがオブジェクトであり,犬がクラスに相当します. ポチは犬です.

もっとプログラミング寄りの次の例で考えると,TASK1がオブジェクトで,タスクがクラスです.

TASK1はタスクです. // ← A 

また,次の例であれば,TM1がオブジェクトで,16ビットタイマーがクラスに相当します.変数とその型の関係によく似ていると考えればよいでしょう.

TM1は16ビットタイマーです. // ← B 

Aで例として挙げたタスクであれば,待ち状態/実行可能状態といった状態や優先度などの属性と,タスクの実行/停止などの振る舞いがあります.また,Bの16ビットタイマーであれば,タイマー値や周期などの属性と,カウントダウンのような振る舞いがあります.このように,通常,クラスは属性と振る舞いを持っています.

なお,C++の言語仕様にも「クラス」という概念がありますが,これはオブジェクト指向プログラミングにおけるクラスをソースコード上で表現するための機能です.もちろん,使い方によっては別の働きをすることもあります.

たとえば,do文は構造化プログラミングにおける反復を表現するためのものですが,次のような使い方をした場合には,反復を表現するものではなくなります.

#define foo(arg)  do { …… } while(0)

同じように,C++のクラスを使ったからといって,必ずしもオブジェクト指向プログラミングにおけるクラスになるわけではありません.また,オブジェクト指向プログラミングにおけるクラスを表現するのに,必ずしもC++のクラスを使わなければならないわけでもありません.ただし,オブジェクト指向プログラミング(ないしはオブジェクト指向設計)のクラスをC++のクラスに対応させたほうがわかりやすいことはいうまでもありません.

カプセル化と情報隠蔽

「カプセル化」というのは,属性と振る舞いを1つにまとめることです.「情報隠蔽」というのは,外部に公開する必要がある情報以外を隠すことです.この両者は別の概念ですが,密接にかかわっています.また,先ほど解説した抽象化やクラスとも密接にかかわっています.というのも,情報隠蔽は,抽象化を実現し,あるいは強化するために必要なのであり,カプセル化は情報隠蔽のために行うものであり,カプセル化を行うためにクラスを使うことになるからです.

やや乱暴に表現すれば,「クラス → カプセル化 → 情報隠蔽 → 抽象化」の順に「手段 → 目的」の関係になっています.ですから,情報隠蔽の役に立たないにもかかわらず,なんでもかんでもカプセル化,すなわちまとめようとするのは間違っています.たとえば,単にデータと関数を集めただけの(それ自体が一種のライブラリのような)ものを作ってみても,それでは本末転倒なのです.逆に,データ構造とアルゴリズムなど,分離できるものは積極的に分離するほうがより良いカプセル化を行うことができますし,なにより使い勝手が良くなります.

抽象的な話ばかりではわかりにくいので,具体例を挙げることにします.抽象化の解説のところでも現れたFILE型を再び取り上げることにしましょう.

FILE型は,デバイスを操作するための情報のほかに,バッファ,ファイル位置表示子,ファイル終了表示子などの属性を持っています.しかし,これらの属性を外部に公開し,直接操作させるのは得策ではありませんから,必ず何らかの関数を介してそれらの属性を操作することになります.FILE型を引数に取る関数はかなりの数がありますが,情報隠蔽のためにカプセル化の対象となる関数は限られています.すなわち,fopen,freopen,fclose,fread,fwrite,fseek,ftell,feof,ferror,clearerr,およびsetvbuf関数さえあれば,あとはこれらの関数を組み合わせることで,他の関数は実装可能なのです*4

実際には,FILE型とそれを扱う関数群は,_open,_close,_read,_write,および_lseekといった,POSIXのシステムコールに相当する低レベル関数によって,もう一段階下の階層でカプセル化され,ヘッダで宣言される関数は,それらを用いて実装されることが多いようです.

カプセル化ないし情報隠蔽によって得られるメリットとしては,まずは抽象化を挙げることができます.すなわち,実装の詳細を知らなくてもそれらを利用することができるようになります.次に,局所性を挙げることができます.すなわち,互いの関係が必要以上に密になることなく,独立性の高いモジュールを作ることができます.これは,関数の中で宣言した局所変数が,他の関数から参照できないことによって得られるメリットと同じです.

多相性

「多相性」はPolymorphismの訳語ですが,「多様性」や「多態性」などと訳されることもあります.また,カタカナで,「ポリモフィズム」や「ポリモルフィズム」と表記されることもありますが,すべて同じ意味です.本書では,C++の標準規格であるJIS X3014がpolymorphicの訳語として「多相」を用いていることから,「多相性」と呼ぶことにします.

多相性というのは,同じ名前の同じ形式の操作が,異なる振る舞いをすることです.

最も簡単な例としては,関数の多重定義や関数テンプレートを挙げることができます.

int abs(int arg);
double abs(double arg);
template <typename T> T abs(T arg);

上記はいずれもabsという同じ名前の関数ですが,実引数の型によって振る舞いが異なります.多相性というと難しく聞こえますが,基本はこういうことです.

先ほどの例は,実引数の静的な型に応じて振る舞いが変わる例でした.しかし,場合によっては動的に振る舞いを変えたいこともあります.ここでもFILE型について考えてみることにしましょう.FILE型は,静的な型はあくまでもFILE型ですが,fopenの引数によって,それがいろいろなデバイスに結び付けられます.同じファイルの場合であっても,フラッシュメモリかもしれませんし,ハードディスクかもしれませんし,フロッピーディスクかもしれません.あるいは,ファイル以外の,たとえば,シリアルポートやプリンタかもしれないわけです.しかし,どんなデバイスに結び付けられようとも,同じ名前の関数を同じ形式で呼び出すことができます.そして,結び付けられたデバイスに応じて適切に振る舞うわけです.これもまた多相性です.通常,特に断ることなく多相性といえば,動的な多相性を指すことが多いようです.JavaやC#などを扱う場合には,特にその傾向が顕著です.

継承

「継承」は,あるクラスを基にして別のクラスを作ることです.このとき,基になったクラスのことを「基底クラス」または「スーパークラス」といい,新しく作ったクラスのことを「派生クラス」または「サブクラス」といいます.

継承によって作られた派生クラスは,基底クラスのすべての性質を受け継ぎ,異なる部分だけを新たに定義すればよいことになります.またまたFILE型について考えてみることにしましょう.ヘッダで提供されるFILE型や関数群を1つのクラスだと考えた場合,それにヘッダで提供されるワイド文字版の関数群(fpuwc関数など)を加えたものが派生クラスだと考えることができます.

しかし,実際には,継承は,単に機能を拡張したり,実装を再利用するためだけの手段ではありません.継承によって作られた派生クラスは,共通部分に関しては基底クラスと同じように扱うことができます.すなわち,基底クラスのインターフェースを再利用するための手段なのです.そして,先に解説した多相性と組み合わせることで,同じインターフェースを使って操作したときの振る舞いを,基底クラスと派生クラスで変えることもできます.

もしかすると,組込み技術者の場合,ハードウェアにたとえたほうが理解しやすいかもしれません.継承というのは,あるマイコンがあった場合,それとピンコンパチで上位互換の命令セットを持ち,内蔵メモリが大きくなった新型のマイコンを作るようなものです.マイコンを実装する基板はそのままで,ICだけを乗せかえればそのまま動作します.プログラムも従来のものがそのまま使えるわけです.あるいは,ピンコンパチで容量も同じだけれども低消費電力のメモリデバイスを考えてもよいでしょう.

*3 あくまでもオブジェクト指向プログラミングに限っての話です.広い意味でのオブジェクト指向,たとえば,オブジェクト指向分析などでは,人や建物や自然現象のような,プログラムとは直接関係のないものもオブジェクトとして扱うことがあります.

*4 厳密にいえば,ワイド文字版の関数群(fgetwc関数など)を実装するためには,もう少し関数が必要です.